const std = @import("std");
const root = @import("root");
const cbor = @import("cbor");
const log = @import("log");
const Style = @import("theme").Style;
const Color = @import("theme").Color;
const vaxis = @import("vaxis");
const input = @import("input");
const builtin = @import("builtin");
const RGB = @import("color").RGB;

pub const Plane = @import("Plane.zig");
pub const Layer = @import("Layer.zig");
pub const Cell = @import("Cell.zig");
pub const CursorShape = vaxis.Cell.CursorShape;

pub const style = @import("style.zig").StyleBits;
pub const styles = @import("style.zig");
const GraphemeCache = @import("GraphemeCache.zig");

const Self = @This();
pub const log_name = "vaxis";

allocator: std.mem.Allocator,

tty: vaxis.Tty,
vx: vaxis.Vaxis,
tty_buffer: []u8,
cache_storage: GraphemeCache.Storage = .{},

no_alternate: bool,
event_buffer: std.Io.Writer.Allocating,
input_buffer: std.Io.Writer.Allocating,
mods: vaxis.Key.Modifiers = .{},
queries_done: bool,

bracketed_paste: bool = false,
bracketed_paste_buffer: std.Io.Writer.Allocating,

handler_ctx: *anyopaque,
dispatch_input: ?*const fn (ctx: *anyopaque, cbor_msg: []const u8) void = null,
dispatch_mouse: ?*const fn (ctx: *anyopaque, y: c_int, x: c_int, cbor_msg: []const u8) void = null,
dispatch_mouse_drag: ?*const fn (ctx: *anyopaque, y: c_int, x: c_int, cbor_msg: []const u8) void = null,
dispatch_event: ?*const fn (ctx: *anyopaque, cbor_msg: []const u8) void = null,

logger: log.Logger,

loop: Loop,

pub const Error = error{
    UnexpectedRendererEvent,
    OutOfMemory,
    IntegerTooLarge,
    IntegerTooSmall,
    InvalidType,
    TooShort,
    Utf8CannotEncodeSurrogateHalf,
    CodepointTooLarge,
    TtyInitError,
    TtyWriteError,
    InvalidFloatType,
    InvalidArrayType,
    InvalidPIntType,
    JsonIncompatibleType,
    NotAnObject,
    BadArrayAllocExtract,
    InvalidMapType,
    InvalidUnion,
    WriteFailed,
} || std.Thread.SpawnError;

pub fn init(allocator: std.mem.Allocator, handler_ctx: *anyopaque, no_alternate: bool, _: *const fn (ctx: *anyopaque) void) Error!Self {
    const opts: vaxis.Vaxis.Options = .{
        .kitty_keyboard_flags = .{
            .disambiguate = true,
            .report_events = true,
            .report_alternate_keys = true,
            .report_all_as_ctl_seqs = true,
            .report_text = true,
        },
        .system_clipboard_allocator = allocator,
    };
    const tty_buffer = try allocator.alloc(u8, 4096);
    return .{
        .allocator = allocator,
        .tty = vaxis.Tty.init(tty_buffer) catch |e| {
            var stderr_buffer: [1024]u8 = undefined;
            var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
            stderr_writer.interface.print("\n" ++ root.application_name ++ " ERROR: {s}\n", .{@errorName(e)}) catch {};
            stderr_writer.interface.flush() catch {};
            return error.TtyInitError;
        },
        .tty_buffer = tty_buffer,
        .vx = try vaxis.init(allocator, opts),
        .no_alternate = no_alternate,
        .event_buffer = .init(allocator),
        .input_buffer = .init(allocator),
        .bracketed_paste_buffer = .init(allocator),
        .handler_ctx = handler_ctx,
        .logger = log.logger(log_name),
        .loop = undefined,
        .queries_done = false,
    };
}

pub fn deinit(self: *Self) void {
    panic_cleanup = null;
    self.loop.stop();
    self.vx.deinit(self.allocator, self.tty.writer());
    self.tty.deinit();
    self.allocator.free(self.tty_buffer);
    self.bracketed_paste_buffer.deinit();
    self.input_buffer.deinit();
    self.event_buffer.deinit();
}

var in_panic: std.atomic.Value(bool) = .init(false);
var panic_cleanup: ?struct {
    allocator: std.mem.Allocator,
    tty: *vaxis.Tty,
    vx: *vaxis.Vaxis,
} = null;

pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
    _ = error_return_trace;
    in_panic.store(true, .release);
    const cleanup = panic_cleanup;
    panic_cleanup = null;
    if (cleanup) |self| {
        self.vx.deinit(self.allocator, self.tty.writer());
        self.tty.deinit();
    }
    return std.debug.defaultPanic(msg, ret_addr orelse @returnAddress());
}

pub fn panic_in_progress() bool {
    return in_panic.load(.acquire);
}

pub fn install_crash_handler() void {
    if (!std.debug.have_segfault_handling_support) {
        @compileError("segfault handler not supported for this target");
    }
    const act = std.posix.Sigaction{
        .handler = .{ .sigaction = handle_crash },
        .mask = std.posix.sigemptyset(),
        .flags = (std.posix.SA.SIGINFO | std.posix.SA.RESTART),
    };

    std.posix.sigaction(std.posix.SIG.BUS, &act, null);
    std.posix.sigaction(std.posix.SIG.SEGV, &act, null);
    std.posix.sigaction(std.posix.SIG.ABRT, &act, null);
    std.posix.sigaction(std.posix.SIG.FPE, &act, null);
    std.posix.sigaction(std.posix.SIG.ILL, &act, null);
}

pub var jit_debugger_enabled: bool = false;

fn handle_crash(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) noreturn {
    const debug = @import("std/debug.zig");
    debug.lockStdErr();

    if (panic_in_progress())
        std.posix.abort();

    in_panic.store(true, .release);
    const cleanup = panic_cleanup;
    panic_cleanup = null;

    if (cleanup) |self| {
        self.vx.deinit(self.allocator, self.tty.writer());
        self.tty.deinit();
    }
    if (builtin.os.tag == .linux and jit_debugger_enabled) {
        var buf: [4096]u8 = undefined;
        var stderr = std.fs.File.stderr().writer(&buf);
        defer stderr.interface.flush() catch {};
        handleSegfaultPosixNoAbort(&stderr.interface, sig, info, ctx_ptr);
        @import("thespian").sighdl_debugger(sig, @ptrCast(@constCast(info)), ctx_ptr);
        std.posix.abort();
    } else {
        debug.handleSegfaultPosix(sig, info, ctx_ptr);
    }
    unreachable;
}

fn handleSegfaultPosixNoAbort(stderr: *std.io.Writer, sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) void {
    const debug = @import("std/debug.zig");
    debug.resetSegfaultHandler();
    const addr = switch (builtin.os.tag) {
        .linux => @intFromPtr(info.fields.sigfault.addr),
        .freebsd, .macos => @intFromPtr(info.addr),
        .netbsd => @intFromPtr(info.info.reason.fault.addr),
        .openbsd => @intFromPtr(info.data.fault.addr),
        .solaris, .illumos => @intFromPtr(info.reason.fault.addr),
        else => unreachable,
    };
    const code = if (builtin.os.tag == .netbsd) info.info.code else info.code;
    debug.dumpSegfaultInfoPosix(stderr, sig, code, addr, ctx_ptr);
}

pub fn run(self: *Self) Error!void {
    self.vx.sgr = .legacy;
    self.vx.enable_workarounds = true;

    panic_cleanup = .{ .allocator = self.allocator, .tty = &self.tty, .vx = &self.vx };
    if (!self.no_alternate) self.vx.enterAltScreen(self.tty.writer()) catch return error.TtyWriteError;
    if (builtin.os.tag == .windows) {
        try self.resize(.{ .rows = 25, .cols = 80, .x_pixel = 0, .y_pixel = 0 }); // dummy resize to fully init vaxis
    } else {
        self.sigwinch() catch return error.TtyWriteError;
    }
    self.vx.setBracketedPaste(self.tty.writer(), true) catch return error.TtyWriteError;
    self.vx.queryTerminalSend(self.tty.writer()) catch return error.TtyWriteError;

    self.loop = Loop.init(&self.tty, &self.vx);
    try self.loop.start();
}

pub fn render(self: *Self) !void {
    if (in_panic.load(.acquire)) return;
    try self.vx.render(self.tty.writer());
    try self.tty.writer().flush();
}

pub fn sigwinch(self: *Self) !void {
    if (builtin.os.tag == .windows or self.vx.state.in_band_resize) return;
    try self.resize(try vaxis.Tty.getWinsize(self.input_fd_blocking()));
}

fn resize(self: *Self, ws: vaxis.Winsize) error{ TtyWriteError, OutOfMemory, WriteFailed }!void {
    self.vx.resize(self.allocator, self.tty.writer(), ws) catch return error.TtyWriteError;
    self.vx.queueRefresh();
    if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"resize"}));
}

pub fn stop(self: *Self) void {
    _ = self;
}

pub fn stdplane(self: *Self) Plane {
    const name = "root";
    var plane: Plane = .{
        .window = self.vx.window(),
        .cache = self.cache_storage.cache(),
        .name_buf = undefined,
        .name_len = name.len,
    };
    @memcpy(plane.name_buf[0..name.len], name);
    return plane;
}

pub fn input_fd_blocking(self: Self) i32 {
    return self.tty.fd;
}

pub fn process_renderer_event(self: *Self, msg: []const u8) Error!void {
    var input_: []const u8 = undefined;
    var text_: []const u8 = undefined;
    if (!try cbor.match(msg, .{ "RDR", cbor.extract(&input_), cbor.extract(&text_) }))
        return error.UnexpectedRendererEvent;
    const text = if (text_.len > 0) text_ else null;
    const event = std.mem.bytesAsValue(vaxis.Event, input_);
    switch (event.*) {
        .key_press => |key__| {
            // Check for a cursor position response for our explicit width query. This will
            // always be an F3 key with shift = true, and we must be looking for queries
            if (key__.codepoint == vaxis.Key.f3 and key__.mods.shift and !self.queries_done) {
                self.logger.print("explicit width capability detected", .{});
                self.vx.caps.explicit_width = true;
                self.vx.caps.unicode = .unicode;
                self.vx.screen.width_method = .unicode;
                return;
            }
            // Check for a cursor position response for our scaled text query. This will
            // always be an F3 key with alt = true, and we must be looking for queries
            if (key__.codepoint == vaxis.Key.f3 and key__.mods.alt and !self.queries_done) {
                self.logger.print("scaled text capability detected", .{});
                self.vx.caps.scaled_text = true;
                return;
            }
            const key_ = filter_mods(normalize_shifted_alphas(key__));
            try self.sync_mod_state(key_.codepoint, key_.mods);
            const cbor_msg = try self.fmtmsg(.{
                "I",
                input.event.press,
                key_.base_layout_codepoint orelse key_.codepoint,
                key_.shifted_codepoint orelse key_.codepoint,
                text orelse "",
                @as(u8, @bitCast(key_.mods)),
            });
            if (self.bracketed_paste and self.handle_bracketed_paste_input(cbor_msg) catch |e| return self.handle_bracketed_paste_error(e)) {
                // we have stored it to handle on .paste_end, so do nothing more here
            } else if (self.dispatch_input) |f| f(self.handler_ctx, cbor_msg);
        },
        .key_release => |key__| {
            const key_ = filter_mods(normalize_shifted_alphas(key__));
            const cbor_msg = try self.fmtmsg(.{
                "I",
                input.event.release,
                key_.base_layout_codepoint orelse key_.codepoint,
                key_.shifted_codepoint orelse key_.codepoint,
                text orelse "",
                @as(u8, @bitCast(key_.mods)),
            });
            if (self.bracketed_paste) {} else if (self.dispatch_input) |f| f(self.handler_ctx, cbor_msg);
        },
        .mouse => |mouse_| {
            const mouse = self.vx.translateMouse(mouse_);
            try self.sync_mod_state(0, .{ .ctrl = mouse.mods.ctrl, .shift = mouse.mods.shift, .alt = mouse.mods.alt });
            if (self.dispatch_mouse) |f| switch (mouse.type) {
                .motion => f(self.handler_ctx, @intCast(mouse.row), @intCast(mouse.col), try self.fmtmsg(.{
                    "M",
                    mouse.col,
                    mouse.row,
                    mouse.xoffset,
                    mouse.yoffset,
                })),
                .press => f(self.handler_ctx, @intCast(mouse.row), @intCast(mouse.col), try self.fmtmsg(.{
                    "B",
                    input.event.press,
                    @intFromEnum(mouse.button),
                    input.utils.button_id_string(mouse.button),
                    mouse.col,
                    mouse.row,
                    mouse.xoffset,
                    mouse.yoffset,
                })),
                .release => f(self.handler_ctx, @intCast(mouse.row), @intCast(mouse.col), try self.fmtmsg(.{
                    "B",
                    input.event.release,
                    @intFromEnum(mouse.button),
                    input.utils.button_id_string(mouse.button),
                    mouse.col,
                    mouse.row,
                    mouse.xoffset,
                    mouse.yoffset,
                })),
                .drag => if (self.dispatch_mouse_drag) |f_|
                    f_(self.handler_ctx, @intCast(mouse.row), @intCast(mouse.col), try self.fmtmsg(.{
                        "D",
                        input.event.press,
                        @intFromEnum(mouse.button),
                        input.utils.button_id_string(mouse.button),
                        mouse.col,
                        mouse.row,
                        mouse.xoffset,
                        mouse.yoffset,
                    })),
            };
        },
        .mouse_leave => {
            if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"mouse_leave"}));
        },
        .focus_in => {
            if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"focus_in"}));
        },
        .focus_out => {
            if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"focus_out"}));
        },
        .paste_start => try self.handle_bracketed_paste_start(),
        .paste_end => try self.handle_bracketed_paste_end(),
        .paste => |_| {
            if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{ "system_clipboard", text }));
        },
        .color_report => {},
        .color_scheme => |scheme| {
            if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{ "color_scheme", scheme }));
        },
        .winsize => |ws| {
            if (!self.vx.state.in_band_resize) {
                self.vx.state.in_band_resize = true;
                self.logger.print("in band resize capability detected", .{});
            }
            try self.resize(ws);
        },

        .cap_unicode => {
            self.logger.print("unicode capability detected", .{});
            self.vx.caps.unicode = .unicode;
            self.vx.screen.width_method = .unicode;
        },
        .cap_sgr_pixels => {
            self.logger.print("pixel mouse capability detected", .{});
            self.vx.caps.sgr_pixels = true;
        },
        .cap_da1 => {
            self.queries_done = true;
            self.vx.enableDetectedFeatures(self.tty.writer()) catch |e| self.logger.err("enable features", e);
            self.vx.setMouseMode(self.tty.writer(), true) catch return error.TtyWriteError;
            self.logger.print("capability queries complete", .{});
            if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"capability_detection_complete"}));
        },
        .cap_kitty_keyboard => {
            self.logger.print("kitty keyboard capability detected", .{});
            self.vx.caps.kitty_keyboard = true;
        },
        .cap_kitty_graphics => {
            if (!self.vx.caps.kitty_graphics) {
                self.vx.caps.kitty_graphics = true;
            }
        },
        .cap_rgb => {
            self.logger.print("rgb capability detected", .{});
            self.vx.caps.rgb = true;
        },
        .cap_color_scheme_updates => {
            self.logger.print("color scheme updates capability detected", .{});
            self.vx.caps.color_scheme_updates = true;
            self.vx.subscribeToColorSchemeUpdates(self.tty.writer()) catch return error.TtyWriteError;
        },
        .cap_multi_cursor => {
            self.logger.print("multi cursor capability detected", .{});
            self.vx.caps.multi_cursor = true;
        },
    }
}

fn fmtmsg(self: *Self, value: anytype) std.Io.Writer.Error![]const u8 {
    self.event_buffer.clearRetainingCapacity();
    try cbor.writeValue(&self.event_buffer.writer, value);
    return self.event_buffer.written();
}

fn handle_bracketed_paste_input(self: *Self, cbor_msg: []const u8) !bool {
    var keypress: input.Key = undefined;
    var egc_: input.Key = undefined;
    var mods: usize = undefined;
    const writer = &self.bracketed_paste_buffer.writer;
    if (try cbor.match(cbor_msg, .{ "I", cbor.number, cbor.extract(&keypress), cbor.extract(&egc_), cbor.string, cbor.extract(&mods) })) {
        switch (keypress) {
            106 => if (mods == 4) try writer.writeAll("\n") else try writer.writeAll("j"),
            input.key.enter => try writer.writeAll("\n"),
            input.key.tab => try writer.writeAll("\t"),
            else => if (!input.is_non_input_key(keypress)) {
                var buf: [6]u8 = undefined;
                const bytes = try input.ucs32_to_utf8(&[_]u32{egc_}, &buf);
                try writer.writeAll(buf[0..bytes]);
            } else {
                var buf: [6]u8 = undefined;
                const bytes = try input.ucs32_to_utf8(&[_]u32{egc_}, &buf);
                self.logger.print("unexpected codepoint in paste: {d} {s}", .{ keypress, buf[0..bytes] });
            },
        }
        return true;
    }
    return false;
}

fn handle_bracketed_paste_start(self: *Self) !void {
    self.bracketed_paste = true;
    self.bracketed_paste_buffer.clearRetainingCapacity();
}

fn handle_bracketed_paste_end(self: *Self) !void {
    defer {
        self.bracketed_paste_buffer.clearRetainingCapacity();
        self.bracketed_paste = false;
    }
    if (!self.bracketed_paste) return;
    if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{ "system_clipboard", self.bracketed_paste_buffer.written() }));
}

fn handle_bracketed_paste_error(self: *Self, e: Error) !void {
    self.logger.err("bracketed paste", e);
    self.bracketed_paste_buffer.clearRetainingCapacity();
    self.bracketed_paste = false;
    return e;
}

pub fn set_terminal_title(self: *Self, text: []const u8) void {
    self.vx.setTitle(self.tty.writer(), text) catch {};
}

pub fn set_terminal_style(self: *Self, style_: Style) void {
    if (style_.fg) |color|
        self.vx.setTerminalForegroundColor(self.tty.writer(), vaxis.Cell.Color.rgbFromUint(@intCast(color.color)).rgb) catch {};
    if (style_.bg) |color|
        self.vx.setTerminalBackgroundColor(self.tty.writer(), vaxis.Cell.Color.rgbFromUint(@intCast(color.color)).rgb) catch {};
}

pub fn set_terminal_cursor_color(self: *Self, color: Color) void {
    self.vx.setTerminalCursorColor(self.tty.writer(), vaxis.Cell.Color.rgbFromUint(@intCast(color.color)).rgb) catch {};
}

pub fn set_terminal_secondary_cursor_color(self: *Self, color: Color) void {
    const rgb = RGB.from_u24(color.color);
    self.tty.writer().print("\x1b[>40;2:{d}:{d}:{d} q", .{ rgb.r, rgb.g, rgb.b }) catch {};
}

pub fn set_terminal_working_directory(self: *Self, absolute_path: []const u8) void {
    self.vx.setTerminalWorkingDirectory(self.tty.writer(), absolute_path) catch {};
}

pub fn copy_to_system_clipboard(self: *Self, text: []const u8) void {
    var writer = self.tty.writer();
    self.vx.copyToSystemClipboard(writer, text, self.allocator) catch |e| log.logger(log_name).err("copy_to_system_clipboard", e);
    writer.flush() catch @panic("flush failed");
}

pub fn request_system_clipboard(self: *Self) void {
    self.vx.requestSystemClipboard(self.tty.writer()) catch |e| log.logger(log_name).err("request_system_clipboard", e);
}

const win32 = struct {
    const windows = std.os.windows;
    pub extern "user32" fn OpenClipboard(hWndNewOwner: ?windows.HWND) callconv(.winapi) windows.BOOL;
    pub extern "user32" fn CloseClipboard() callconv(.winapi) windows.BOOL;
    pub extern "user32" fn SetClipboardData(uFormat: windows.UINT, hMem: windows.HANDLE) callconv(.winapi) ?windows.HANDLE;
    pub extern "user32" fn GetClipboardData(uFormat: windows.UINT) callconv(.winapi) ?windows.HANDLE;
    pub extern "user32" fn EmptyClipboard() windows.BOOL;
    pub extern "kernel32" fn GlobalAlloc(flags: c_int, size: usize) ?windows.HANDLE;
    pub extern "kernel32" fn GlobalFree(hMem: windows.HANDLE) windows.BOOL;
    pub extern "kernel32" fn GlobalLock(hMem: windows.HANDLE) ?windows.LPVOID;
    pub extern "kernel32" fn GlobalUnlock(hMem: windows.HANDLE) windows.BOOL;
    const CF_TEXT = @as(c_int, 1);
    const GMEM_MOVEABLE = @as(c_int, 2);
};

pub fn copy_to_windows_clipboard(text: []const u8) !void {
    const mem = win32.GlobalAlloc(win32.GMEM_MOVEABLE, text.len + 1) orelse return error.GlobalAllocFalied;
    const data: [*c]u8 = @ptrCast(win32.GlobalLock(mem) orelse return error.ClipboardDataLockFailed);
    @memcpy(data[0..text.len], text);
    data[text.len] = 0;
    _ = win32.GlobalUnlock(mem);

    if (win32.OpenClipboard(null) == 0) {
        _ = win32.GlobalFree(mem);
        return error.OpenClipBoardFailed;
    }
    defer _ = win32.CloseClipboard();

    _ = win32.EmptyClipboard();
    if (win32.SetClipboardData(win32.CF_TEXT, mem) == null) {
        _ = win32.GlobalFree(mem);
    }
}

pub fn request_windows_clipboard(allocator: std.mem.Allocator) ![]u8 {
    if (win32.OpenClipboard(null) == 0)
        return error.OpenClipBoardFailed;
    defer _ = win32.CloseClipboard();

    const mem = win32.GetClipboardData(win32.CF_TEXT) orelse return error.ClipboardDataRetrievalFailed;
    const data: [*c]u8 = @ptrCast(win32.GlobalLock(mem) orelse return error.ClipboardDataLockFailed);
    const text = std.mem.span(data);
    defer _ = win32.GlobalUnlock(mem);

    return allocator.dupe(u8, text);
}

pub fn request_mouse_cursor_text(self: *Self, push_or_pop: bool) void {
    if (push_or_pop) self.vx.setMouseShape(.text) else self.vx.setMouseShape(.default);
}

pub fn request_mouse_cursor_pointer(self: *Self, push_or_pop: bool) void {
    if (push_or_pop) self.vx.setMouseShape(.pointer) else self.vx.setMouseShape(.default);
}

pub fn request_mouse_cursor_default(self: *Self, push_or_pop: bool) void {
    if (push_or_pop) self.vx.setMouseShape(.default) else self.vx.setMouseShape(.default);
}

pub fn cursor_enable(self: *Self, y: c_int, x: c_int, shape: CursorShape) !void {
    self.vx.screen.cursor_vis = true;
    self.vx.screen.cursor_row = @intCast(y);
    self.vx.screen.cursor_col = @intCast(x);
    self.vx.screen.cursor_shape = shape;
}

pub fn cursor_disable(self: *Self) void {
    self.vx.screen.cursor_vis = false;
}

pub fn clear_all_multi_cursors(self: *Self) !void {
    try self.tty.writer().print("\x1b[>0;4 q", .{});
}

pub fn show_multi_cursor_yx(self: *Self, y: c_int, x: c_int) !void {
    try self.tty.writer().print("\x1b[>29;2:{d}:{d} q", .{ y + 1, x + 1 });
}

fn sync_mod_state(self: *Self, keypress: u32, modifiers: vaxis.Key.Modifiers) !void {
    if (modifiers.ctrl and !self.mods.ctrl and !(keypress == input.key.left_control or keypress == input.key.right_control))
        try self.send_sync_key(input.event.press, input.key.left_control, "", modifiers);
    if (!modifiers.ctrl and self.mods.ctrl and !(keypress == input.key.left_control or keypress == input.key.right_control))
        try self.send_sync_key(input.event.release, input.key.left_control, "", modifiers);
    if (modifiers.alt and !self.mods.alt and !(keypress == input.key.left_alt or keypress == input.key.right_alt))
        try self.send_sync_key(input.event.press, input.key.left_alt, "", modifiers);
    if (!modifiers.alt and self.mods.alt and !(keypress == input.key.left_alt or keypress == input.key.right_alt))
        try self.send_sync_key(input.event.release, input.key.left_alt, "", modifiers);
    if (modifiers.shift and !self.mods.shift and !(keypress == input.key.left_shift or keypress == input.key.right_shift))
        try self.send_sync_key(input.event.press, input.key.left_shift, "", modifiers);
    if (!modifiers.shift and self.mods.shift and !(keypress == input.key.left_shift or keypress == input.key.right_shift))
        try self.send_sync_key(input.event.release, input.key.left_shift, "", modifiers);
    self.mods = modifiers;
}

fn send_sync_key(self: *Self, event: input.Event, keypress: u32, key_string: []const u8, modifiers: vaxis.Key.Modifiers) !void {
    if (self.dispatch_input) |f| f(
        self.handler_ctx,
        try self.fmtmsg(.{
            "I",
            event,
            keypress,
            keypress,
            key_string,
            @as(u8, @bitCast(modifiers)),
        }),
    );
}

fn filter_mods(key_: vaxis.Key) vaxis.Key {
    var key__ = key_;
    key__.mods = .{
        .shift = key_.mods.shift,
        .alt = key_.mods.alt,
        .ctrl = key_.mods.ctrl,
    };
    return key__;
}

fn normalize_shifted_alphas(key_: vaxis.Key) vaxis.Key {
    if (!key_.mods.shift) return key_;
    var key = key_;
    const shifted_codepoint = key.shifted_codepoint orelse key.codepoint;
    const base_layout_codepoint = key.base_layout_codepoint orelse key.codepoint;
    if (shifted_codepoint == base_layout_codepoint and 'a' <= shifted_codepoint and shifted_codepoint <= 'z')
        key.shifted_codepoint = shifted_codepoint - 0x20;
    return key;
}

const Loop = struct {
    tty: *vaxis.Tty,
    vaxis: *vaxis.Vaxis,
    pid: tp.pid,

    thread: ?std.Thread = null,
    should_quit: bool = false,

    const tp = @import("thespian");

    pub fn init(tty: *vaxis.Tty, vaxis_: *vaxis.Vaxis) Loop {
        return .{
            .tty = tty,
            .vaxis = vaxis_,
            .pid = tp.self_pid().clone(),
        };
    }

    pub fn deinit(self: *Loop) void {
        self.pid.deinit();
    }

    /// spawns the input thread to read input from the tty
    pub fn start(self: *Loop) std.Thread.SpawnError!void {
        if (self.thread) |_| return;
        self.thread = try std.Thread.spawn(.{}, Loop.ttyRun, .{self});
    }

    /// stops reading from the tty.
    pub fn stop(self: *Loop) void {
        self.should_quit = true;
        // trigger a read
        self.vaxis.deviceStatusReport(self.tty.writer()) catch {};

        if (self.thread) |thread| {
            thread.join();
            self.thread = null;
            self.should_quit = false;
        }
    }

    fn postEvent(self: *Loop, event: vaxis.Event) void {
        var text: []const u8 = "";
        var free_text: bool = false;
        switch (event) {
            .key_press => |key_| {
                if (key_.text) |text_| text = text_;
            },
            .key_release => |key_| {
                if (key_.text) |text_| text = text_;
            },
            .paste => |text_| {
                text = text_;
                free_text = true;
            },
            else => {},
        }
        self.pid.send(.{ "RDR", std.mem.asBytes(&event), text }) catch @panic("send RDR event failed");
        if (free_text)
            self.vaxis.opts.system_clipboard_allocator.?.free(text);
    }

    fn ttyRun(self: *Loop) !void {
        switch (builtin.os.tag) {
            .windows => {
                var parser: vaxis.Parser = .{};
                const a = self.vaxis.opts.system_clipboard_allocator orelse @panic("no tty allocator");
                while (!self.should_quit) {
                    self.postEvent(try self.tty.nextEvent(&parser, a));
                }
            },
            else => {
                var parser: vaxis.Parser = .{};

                const a = self.vaxis.opts.system_clipboard_allocator orelse @panic("no tty allocator");

                var buf = try a.alloc(u8, 512);
                defer a.free(buf);
                var n: usize = 0;
                var need_read = false;

                while (!self.should_quit) {
                    if (n >= buf.len) {
                        const buf_grow = try a.alloc(u8, buf.len * 2);
                        @memcpy(buf_grow[0..buf.len], buf);
                        a.free(buf);
                        buf = buf_grow;
                    }
                    if (n == 0 or need_read) {
                        const n_ = try self.tty.read(buf[n..]);
                        n = n + n_;
                        need_read = false;
                    }
                    const result = try parser.parse(buf[0..n], a);
                    if (result.n == 0) {
                        need_read = true;
                        continue;
                    }
                    if (result.event) |event| {
                        self.postEvent(event);
                    }
                    if (result.n < n) {
                        const buf_move = try a.alloc(u8, buf.len);
                        @memcpy(buf_move[0 .. n - result.n], buf[result.n..n]);
                        a.free(buf);
                        buf = buf_move;
                        n = n - result.n;
                    } else {
                        n = 0;
                    }
                }
            },
        }
    }
};
